/*
	Name: SPUtility.js
	Version: 0.8.1
	Description: 
		A JavaScript library that is used to alter SharePoint's user interface
		(mostly NewForm and EditForm). It can be used to populate fields, make
		fields read only, or hide a field from view.
	Author: Kit Menke
	http://SPUtility.codeplex.com/
	License: Microsoft Public License (see http://sputility.codeplex.com/license)
	Changelog: http://sputility.codeplex.com/wikipage?title=Changelog
*/

/*jslint white: true, browser: true, devel: true, onevar: true, undef: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, indent: 3 */
/*global Element: false, Class: false, $: false, $$: false, $A: false, Hash: false, Event: false, Prototype: false, $break: false, $H: false, RTE_GetIFrameContents: false, RTE_TransferTextAreaContentsToIFrame: false, RTE_GetEditorIFrame: false */


// getTextContent credit to Dan Dean
// http://dandean.com/category/code/2009/hatin-on-textcontent-and-innertext/
Element.addMethods({
	/**
	*  Element#getTextContent(@element) -> String
	*  Cross-browser means of getting Element#textContent or Element#innerText
	**/
	getTextContent: function (element) {
		if (!Object.isUndefined(element.textContent)) {
			return element.textContent;
		}
		return element.innerText;
	},
	
	setTextContent: function (element, value) {
		if (!Object.isUndefined(element.textContent)) {
			element.textContent = value;
		}
		element.innerText = value;
	}
});

//+ Jonas Raoni Soares Silva
//@ http://jsfromhell.com/number/fmt-money [rev. #2]
// Modified to pass JSLint
// c = # of floating point decimal places
// d = decimal separator
// t = thousands separator
Number.prototype.formatMoney = function (c, d, t) {
	c = (isNaN(c = Math.abs(c)) ? 2 : c);
	d = (d === undefined ? "." : d);
	t = (t === undefined ? "," : t);
	var n = this, 
		s = (n < 0 ? "-" : ""),
		i = parseInt(n = Math.abs(+n || 0).toFixed(c), 10) + "", 
		j = (j = i.length) > 3 ? j % 3 : 0;
	return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
};

/*
 *	SPUtility namespace
 */
var SPUtility = (function () {
	"use strict";
	
	/*
	 *	SPUtility Private Variables
	**/
	
	var SPField, SPTextField, SPUserField, SPURLField, SPChoiceField, 
		SPDateTimeField, SPDateTimeFieldValue, SPFileField, SPCurrencyField, 
		SPNumberField, SPBooleanField, SPNoteField, SPLookupField, SPLookupMultiField,
		_fieldsHashtable = null,
		_debugMode = false,
		_isSurveyForm = false,
		_numErrors = 0; 
	
	/*
	 *	SPUtility Private Methods
	**/
	
	function log(message, exception) {
		var property;
		try {
			if (!_debugMode) {
				return;
			}
			if (exception) {
				message += '\r\n';
				for (property in exception) {
					if (exception.hasOwnProperty(property)) {
						message += property + ': ' + exception[property] + '\r\n';
					}
				}
			}
			if (Prototype.Browser.IE || Object.isUndefined(console)) {
				_numErrors += 1;
				if (_numErrors === 3) {
					message = "More than 3 errors (additional errors will not be shown):\r\n" + message;
				}
				if (_numErrors <= 3) {
					alert(message);
				}
			} else {
				console.error(message);
			}
		} catch (ex) { }
	}
	
	function convertStringToNumber(val) {
		if (typeof val === "string") {
			var match = val.match(/[0-9,.]+/g);
			if (null !== match) {
				val = match[0].replace(/,/g, ''); // commas to delimit thousands need to be removed
				val = parseFloat(val);
			}
		}
		return val;
	}
	
	// Gets the input controls for a field (used for Textboxes)
	function getInputControl(spField) {
		var controls = spField.Controls.select('input');
		if (null !== controls && 1 === controls.length) {
			return controls[0];
		}
		
		throw 'Unable to retrieve the input control for ' + spField.Name;
	}
	
	function getHashFromInputControls(spField, selector) {
		var oHash = null, inputTags = spField.Controls.select(selector), inputLabel, key;
		if (null !== inputTags && inputTags.length > 0) {
			oHash = new Hash();
			
			inputTags.each(function (elem) {
				inputLabel = elem.next(0);
				if (!Object.isUndefined(inputLabel)) {
					key = inputLabel.getTextContent();
					oHash.set(key, elem);
				}
			});
		}
		return oHash;
	}
	
	function getSPFieldType(element) {
		var matches, comment, n;
		try {
			// find the HTML comment and get the field's type
			for (n = 0; n < element.childNodes.length; n += 1) {
				if (8 === element.childNodes[n].nodeType) {
					comment = element.childNodes[n].data;
					matches = comment.match(/SPField\w+/);
					if (null !== matches) {
						return matches[0];
					}
					break;
				}
			}			
		} catch (ex) {
			log('getSPFieldType: Error getting field type', ex);
		}
		return null;
	}
	
	function getSPFieldFromType(spFieldParams) {
		var field = null;
		
		switch (spFieldParams.type) {
		case 'SPFieldText':
			field = new SPTextField(spFieldParams);
			break;
		case 'SPFieldNote':
			field = new SPNoteField(spFieldParams);
			break;
		case 'SPFieldBoolean':
			field = new SPBooleanField(spFieldParams);
			break;
		case 'SPFieldNumber':
			field = new SPNumberField(spFieldParams);
			break;
		case 'SPFieldCurrency':
			field = new SPCurrencyField(spFieldParams);
			break;
		case 'SPFieldFile':
			field = new SPFileField(spFieldParams);
			break;
		case 'SPFieldDateTime':
			field = new SPDateTimeField(spFieldParams);
			break;
		case 'SPFieldChoice':
		case 'SPFieldMultiChoice':
			field = new SPChoiceField(spFieldParams);
			break;
		case 'SPFieldURL':
			field = new SPURLField(spFieldParams);
			break;
		case 'SPFieldUser':
		case 'SPFieldUserMulti':
			field = new SPUserField(spFieldParams);
			break;
		case 'SPFieldLookup':
			field = new SPLookupField(spFieldParams);
			break;
		case 'SPFieldLookupMulti':
			field = new SPLookupMultiField(spFieldParams);
			break;
		default:
			field = new SPField(spFieldParams);
			break;
		}
		return field;
	}
	
	function createSPField(spFieldParams) {
		var field = null;
		try {
			if (null === spFieldParams.controlsCell) {
				// the only time this property will NOT be null is in survey forms
				spFieldParams.controlsCell = spFieldParams.labelCell.next();
				// use nextSibling?
			}
			spFieldParams.type = getSPFieldType(spFieldParams.controlsCell);
			
			// if we can't get the type then we can't create the field
			if (null === spFieldParams.type) {
				return null;
			}
			
			field = getSPFieldFromType(spFieldParams);
		} catch (e) {
			log('createSPField: Error creating field for ' + spFieldParams.name, e);
		}
		return field;
	}
	
	function getFieldParams(elemTD, surveyElemTD, isSurveyForm) {
		var fieldParams = null, fieldName = 'Unknown field', elemLabel, isRequired;
		try {
			if (isSurveyForm) {
				elemLabel = elemTD;
			} else {
				// navigate TD -> ??
				elemLabel = elemTD.firstDescendant();
				if (null === elemLabel || elemLabel.nodeName === 'NOBR') {
					return null; // attachments row not currently supported
				}
			}
			
			fieldName = (elemLabel.getTextContent()).strip();
			isRequired = fieldName.endsWith(' *');
			
			if (true === isRequired) {
				fieldName = fieldName.substring(0, fieldName.length - 2);
			}
			
			fieldParams = {
				'name': fieldName,
				'label': $(elemLabel),
				'labelRow': $(elemTD.parentNode),
				'labelCell': elemTD,
				'isRequired': isRequired,
				'controlsRow': Object.isUndefined(surveyElemTD) ? null : $(surveyElemTD.parentNode),
				'controlsCell': Object.isUndefined(surveyElemTD) ? null : surveyElemTD,
				'type': null,
				'spField': null
			};
		} catch (e) {
			log('getFieldParams: Error getting field parameters ' + fieldName, e);
		}
		return fieldParams;
	}
	
	function lazyLoadSPFields() {
		if (null === _fieldsHashtable) {
			var i, fieldParams,
				fieldElements = $$('table.ms-formtable td.ms-formlabel'),
				surveyElements = $$('table.ms-formtable td.ms-formbodysurvey'),
				len = fieldElements.length;

			_isSurveyForm = (surveyElements.length > 0);
			_fieldsHashtable = new Hash();
			
			for (i = 0; i < len; i += 1) {
				fieldParams = getFieldParams(fieldElements[i], surveyElements[i], _isSurveyForm);
				if (null !== fieldParams) {
					_fieldsHashtable.set(fieldParams.name, fieldParams);
				}
			}
		}
	}
	
	function toggleSPFieldRows(labelRow, controlsRow, bShowField) {
		// controlsRow is populated on survey forms (null otherwise)
		if (bShowField) {
			labelRow.show();
			if (null !== controlsRow) {
				controlsRow.show();
			}
		} else {
			labelRow.hide();
			if (null !== controlsRow) {
				controlsRow.hide();
			}
		}
	}
	
	function toggleSPField(strFieldName, bShowField) {
		lazyLoadSPFields();
			
		var fieldParams = _fieldsHashtable.get(strFieldName);
		
		if (Object.isUndefined(fieldParams)) { 
			log('toggleSPField: Unable to find a SPField named ' + strFieldName + ' - ' + bShowField);
			return;
		}
		
		toggleSPFieldRows(fieldParams.labelRow, fieldParams.controlsRow, bShowField);
	}
	
	function updateReadOnlyLabel(spField, value) {
		if (spField.ReadOnlyLabel) {
			spField.ReadOnlyLabel.update(spField.GetValue());
		}
	}
	
	function makeReadOnly(spField, htmlToInsert) {
		try {
			Element.hide(spField.Controls);
			if (null === spField.ReadOnlyLabel) {
				spField.ReadOnlyLabel = new Element('div');
				spField.ReadOnlyLabel.addClassName('sputility-readonly');
				Element.insert(spField.Controls, { after: spField.ReadOnlyLabel });
			}
			spField.ReadOnlyLabel.update(htmlToInsert);
			spField.ReadOnlyLabel.show();
		} catch (ex) {
			alert('Error making ' + spField.Name + ' read only. ' + ex.toString());
		}
		return spField;
	}
	
	function arrayToSemicolonList(arr) {
		var text = '';
		
		arr.each(function (value) {
			text += value + '; ';
		});
		
		if (text.length > 2) {
			text = text.substring(0, text.length - 2);
		}
		
		return text;
	}
	
	/*
	 *	SPUtility Classes
	**/
	
	/*
	 *	SPField class
	 *	Contains all of the common properties and functions used by the specialized
	 *	sub-classes. Typically, this should not be intantiated directly.
	 */
	SPField = Class.create({
		initialize: function (fieldParams) {
			// Public Properties
			this.Label = fieldParams.label;
			this.LabelRow = fieldParams.labelRow;
			this.Name = fieldParams.name;
			this.IsRequired = fieldParams.isRequired;
			this.Type = fieldParams.type;
			this.Controls = fieldParams.controlsCell.firstDescendant();
			this.ControlsRow = fieldParams.controlsRow;
			this.ReadOnlyLabel = null;
		},
		
		/*
		 *	Public SPField Methods
		 */
		Show: function () {
			toggleSPFieldRows(this.LabelRow, this.ControlsRow, true);
			return this;
		},
		
		Hide: function () {
			toggleSPFieldRows(this.LabelRow, this.ControlsRow, false);
			return this;
		},
		
		MakeReadOnly: function () {
			return makeReadOnly(this, this.GetValue().toString());
		},
		  
		MakeEditable: function () {
			try {
				Element.show(this.Controls);
				if (null !== this.ReadOnlyLabel) {
					this.ReadOnlyLabel.hide();
				}
			} catch (ex) {
				alert('Error making ' + this.Name + ' editable. ' + ex.toString());
			}
			return this;
		},
		
		toString: function () {
			return this.Name;		
		},
		
		/*
		 *	Public SPField Override Methods
		 *	All of the below methods need to be implemented in each sub-class
		 */
		GetValue: function () {
			throw 'GetValue not yet implemented for ' + this.Type + ' in ' + this.Name;
		},
		
		SetValue: function (value) {
			throw 'SetValue not yet implemented for ' + this.Type + ' in ' + this.Name;
		}
	});
	
	/*
	 *	SPTextField class
	 *	Supports Single line of text and Currency fields (base class for number fields)
	 */
	SPTextField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.Textbox = getInputControl(this);
		},
		
		/*
		 *	SPTextField Public Methods
		 *	Overrides SPField class methods.
		 */
		GetValue: function () {
			return this.Textbox.getValue();
		},
		
		SetValue: function (value) {
			this.Textbox.setValue(value);
			updateReadOnlyLabel(this);
			return this;
		}
	});
	
	/*
	 *	SPLookupField class
	 *	Supports single select lookup fields
	 */
	SPLookupField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			var controls = this.Controls.select('input');
			if (1 === controls.length) {
				// autocomplete lookup
				this.Textbox = controls[0];
				
				this.GetValue = function () {
					return this.Textbox.getValue();
				};
				
				this.SetValue = function (value) {
					var choices, hash;
					
					if (Object.isNumber(value)) {
						// a list item ID was passed to the function so attempt to lookup the text value
						choices = this.Textbox.readAttribute('choices');
						hash = new Hash();
						// JSLint error here but not much I can do...
						choices.scan(/(([^\|]|\|\|)+)\|(\d+)/, function (match) {
							hash.set(match[3], match[1].replace(/\|\|/g, '|'));
						});

						this.Textbox.setValue(hash.get(value.toString()));
					} else {
						this.Textbox.setValue(value);
					}
					
					updateReadOnlyLabel(this);
					return this;
				};
				
			} else {
				controls = this.Controls.select('select');
				if (1 === controls.length) {
					// regular dropdown lookup
					this.Dropdown = controls[0];
					
					this.GetValue = function () {
						return this.Dropdown.options[this.Dropdown.selectedIndex].text;
					};
					
					this.SetValue = function (value) {
						if (Object.isNumber(value)) {
							this.Dropdown.setValue(value);
						} else {
							var i, numOptions;
							// need to set the dropdown based on text
							numOptions = this.Dropdown.options.length;
							for (i = 0; i < numOptions; i += 1) {
								if (this.Dropdown.options[i].text === value) {
									this.Dropdown.selectedIndex = i;
									break;
								}
							}
						}
						updateReadOnlyLabel(this);
						return this;
					};
				}
			}
		}
	});
	
	/*
	 *	SPLookupMultiField class
	 *	Supports multi select lookup fields
	 */
	SPLookupMultiField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			var controls = this.Controls.select('select');
			if (2 === controls.length) {
				// multi-select lookup
				this.ListChoices = controls[0];
				this.ListSelections = controls[1];
				controls = this.Controls.select('button');
				this.ButtonAdd = controls[0];
				this.ButtonRemove = controls[1];
				
				this.GetValue = function () {
					var values = [], i, numOptions;
					
					numOptions = this.ListSelections.options.length;
					for (i = 0; i < numOptions; i += 1) {
						values.push(this.ListSelections.options[i].text);
					}
					
					return values;
				};
				
				// display as semicolon delimited list
				this.MakeReadOnly = function (options) {
					return makeReadOnly(this, arrayToSemicolonList(this.GetValue()));
				};
				
				this.SetValue = function (value, addValue) {
					if (Object.isUndefined(addValue)) {
						addValue = true;
					}
					
					var i, option, options, numOptions, funcAction;
					
					if (addValue) {
						options = this.ListChoices.options;
						funcAction = this.ButtonAdd.onclick;
					} else {
						options = this.ListSelections.options;
						funcAction = this.ButtonRemove.onclick;
					}
					
					numOptions = options.length;
					
					// select the value
					if (Object.isNumber(value)) {
						value = value.toString();
						for (i = 0; i < numOptions; i += 1) {
							option = options[i];
							
							// deliberate fuzzy comparison
							if (option.value === value) {
								option.selected = true;
							} else {
								option.selected = false;
							}
						}
					} else {
						for (i = 0; i < numOptions; i += 1) {
							option = options[i];
							
							// deliberate fuzzy comparison
							if (option.text === value) {
								option.selected = true;
							} else {
								option.selected = false;
							}
						}
					}
					
					funcAction(); // add or remove the value
					
					updateReadOnlyLabel(this);
					return this;
				};
			}
		}
	});
	
	/*
	 *	SPNoteField class
	 *	Supports rich text fields (SPFieldNote)
	 */
	SPNoteField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.Textbox = null;
			var controls = this.Controls.select('textarea');
			if (1 === controls.length) {
				this.Textbox = controls[0];
			}
			
			this.IsRichText = (RTE_GetEditorIFrame(this.Textbox.id) !== null);
			
			if (this.IsRichText) {
				// RTE functions are defined in layouts/1033/form.js
				this.GetValue = function () {
					return RTE_GetIFrameContents(this.Textbox.id);
				};
				
				this.SetValue = function (value) {
					this.Textbox.setValue(value);
					RTE_TransferTextAreaContentsToIFrame(this.Textbox.id);
					updateReadOnlyLabel(this);
					return this;
				};
			} else {
				this.GetValue = function () {
					return this.Textbox.getValue();
				};
				
				this.SetValue = function (value) {
					this.Textbox.setValue(value);
					updateReadOnlyLabel(this);
					return this;
				};
			}
		}
		
	});

	/*
	 *	SPBooleanField class
	 *	Supports yes/no fields (SPFieldBoolean)
	 */
	SPBooleanField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.Checkbox = getInputControl(this);
		},
		
		/*
		 *	SPBooleanField Public Methods
		 *	Overrides SPField class methods.
		 */
		GetValue: function () {
			// double negative to return a boolean value
			return !!this.Checkbox.getValue();
		},
		
		SetValue: function (value) {
			this.Checkbox.setValue(value);
			updateReadOnlyLabel(this);
			return this;
		}
	});

	
	/*
	 *	SPNumberField class
	 *	Supports Number fields
	 */
	SPNumberField = Class.create(SPTextField, {
		initialize: function ($super, spParams) {
			$super(spParams);
		},
		
		/*
		 *	SPNumberField Public Methods
		 *	Overrides SPTextField class methods.
		 */
		GetValue: function () {
			return convertStringToNumber(this.Textbox.getValue());
		}
	});
	
	/*
	 *	SPCurrencyField class
	 *	Supports currency fields (SPCurrencyField)
	 */
	SPCurrencyField = Class.create(SPNumberField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			this.FormatOptions = {
				eventHandler: null,
				autoCorrect: false,
				decimalPlaces: 2
			};
		},
		
		Format: function () {
			if (this.FormatOptions.autoCorrect) {
				this.FormatOptions.eventHandler = function () {
					this.SetValue(this.GetFormattedValue());
				}.bindAsEventListener(this);
				Event.observe(this.Textbox, 'change', this.FormatOptions.eventHandler);
				this.FormatOptions.eventHandler(); // run once
			} else {
				if (this.FormatOptions.eventHandler) {
					Event.stopObserving(this.Textbox, 'change', this.FormatOptions.eventHandler);
					this.FormatOptions.eventHandler = null;
				}
			}
		},
		
		GetFormattedValue: function () {
			var text = this.GetValue();
			if (typeof text === "number") {
				text = '$' + text.formatMoney(this.FormatOptions.decimalPlaces);
			}
			return text;
		},
		
		// Override the default MakeReadOnly function to allow displaying
		// the value with currency symbols
		MakeReadOnly: function (options) {
			return makeReadOnly(this, this.GetFormattedValue());
		}
	});
	
	/*
	 *	SPUserField class
	 *	Supports people fields (SPFieldUser)
	 */
	SPUserField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.spanUserField = null;
			this.upLevelDiv = null;
			this.textareaDownLevelTextBox = null;
			this.linkCheckNames = null;
			this.txtHiddenSpanData = null;
						
			var controls = this.Controls.select('span.ms-usereditor');
			if (null !== controls && 1 === controls.length) {
				this.spanUserField = controls[0];
				this.upLevelDiv = $(this.spanUserField.id + '_upLevelDiv');
				this.textareaDownLevelTextBox = $(this.spanUserField.id + '_downlevelTextBox');
				this.linkCheckNames = $(this.spanUserField.id + '_checkNames');
				this.txtHiddenSpanData = $(this.spanUserField.id + '_hiddenSpanData');
			}
		},
		
		/*
		 *	SPUserField Public Methods
		 *	Overrides SPField class methods.
		 */
		GetValue: function () {
			//this.textareaDownLevelTextBox.getValue()
			return this.upLevelDiv.getTextContent();
		},
		
		SetValue: function (value) {
			if (Prototype.Browser.IE) {
				this.upLevelDiv.innerHTML = value;
				this.txtHiddenSpanData.setValue(value);
				this.linkCheckNames.click();
			} else { // FireFox (maybe others?)
				this.textareaDownLevelTextBox.setValue(value);
				this.linkCheckNames.onclick();
			}
			updateReadOnlyLabel(this);
			return this;
		}
	});
	
	/*
	 *	SPURLField class
	 *	Supports hyperlink fields (SPFieldURL)
	 */
	SPURLField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.TextboxURL = null;
			this.TextboxDescription = null;
			
			var controls = this.Controls.select('input');
			if (null !== controls && 2 === controls.length) {
				this.TextboxURL = controls[0];
				this.TextboxDescription = controls[1];
			}
		},
		
		/*
		 *	SPURLField Public Methods
		 *	Overrides SPField class methods.
		 */
		GetValue: function () {
			return [this.TextboxURL.getValue(), this.TextboxDescription.getValue()];
		},
		
		SetValue: function (url, description) {
			this.TextboxURL.setValue(url);
			this.TextboxDescription.setValue(description);
			updateReadOnlyLabel(this);
			return this;
		},
		
		// overriding the default MakeReadOnly function because we have multiple values returned
		// and we want to have the hyperlink field show up as a URL
		MakeReadOnly: function (options) {
			var text, values = this.GetValue();
			
			if (options && true === options.TextOnly) {
				text = values[0] + ', ' + values[1];
			} else {				
				text = '<a href="' + values[0] + '">' + values[1] + '</a>';
			}
			
			return makeReadOnly(this, text);
		}
	});
	
	/*
	 *	SPFileField class
	 *	Supports the name field of a Document Library
	 */
	SPFileField = Class.create(SPTextField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.FileExtension = this.Textbox.up(0).getTextContent();
		},
		
		/*
		 *	SPFileField Public Methods
		 *	Overrides SPField class methods.
		 */
		GetValue: function () {
			return this.Textbox.getValue() + this.FileExtension;
		}
	});
	
	/*
	 *	SPChoiceField class
	 *	Supports single select choice fields that show as either a dropdown or radio buttons
	 */
	SPChoiceField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			var FILL_IN_VALUE = 'Specify your own value:',
				CHECKBOX_SELECTOR = 'input[type="checkbox"]',
				RADIO_SELECTOR = 'input[type="radio"]',
				controls = null;
			
			$super(spParams);
			
			this.FillInTextbox = this.Controls.select('input[type="text"]');
			if (this.FillInTextbox.length === 1) {
				this.FillInTextbox = this.FillInTextbox[0];
				this.FillInAllowed = true;
				this.FillInElement = null;
			} else {
				this.FillInAllowed = false;
				this.FillInTextbox = null;
				this.FillInElement = null;
			}
			
			// is this a single select or multi-select?
			if (this.Type === 'SPFieldChoice') {
				// single select choice field using a dropdown
				controls = this.Controls.select('select');
				if (1 === controls.length) {
					this.Dropdown = controls[0];
					
					if (!this.FillInAllowed) {
						// dynamically set our getter/setter functions
						this.GetValue = function () {
							return this.Dropdown.getValue();
						};
						this.SetValue = function (value) {
							this.Dropdown.setValue(value);
							updateReadOnlyLabel(this);
							return this;
						};
					} else {
						// dropdown with a fill-in value
						controls = getHashFromInputControls(this, RADIO_SELECTOR);
						this.FillInElement = controls.get(FILL_IN_VALUE);
						controls.unset(FILL_IN_VALUE);
						
						this.GetValue = function () {
							if (this.FillInElement.checked === true) {
								return this.FillInTextbox.getValue();
							}
							return this.Dropdown.getValue();
						};
						
						this.SetValue = function (value) {
							var found = false;
							found = !Object.isUndefined($A(this.Dropdown.options).find(function (option) { return option.value === value; }));
							if (found) {
								this.Dropdown.setValue(value);
								this.FillInElement.checked = false;
								// fires the function to select the radio button
								this.Dropdown.onclick();
							} else {
								this.FillInElement.checked = true;
								this.FillInTextbox.setValue(value);
							}
							updateReadOnlyLabel(this);
							return this;
						};
					}
				}
			}
			
			// if the field is single select, then we will have already found
			// a dropdown control and everything is setup. otherwise we have setup to do
			if (Object.isUndefined(this.Dropdown)) {
				if (this.Type === 'SPFieldMultiChoice') {
					// multi-choice fields use checkboxes
					this.Checkboxes = getHashFromInputControls(this, CHECKBOX_SELECTOR);
					
					// remove the fill-in checkbox because we will check it separately
					if (this.FillInAllowed) {
						this.FillInElement = this.Checkboxes.get(FILL_IN_VALUE);
						this.Checkboxes.unset(FILL_IN_VALUE);
					}
					
					this.GetValue = function () {
						var values = [];
						
						this.Checkboxes.each(function (pair) {
							var checkbox = pair.value;
							if (checkbox.checked === true) {
								values.push(pair.key);
							}
						});
						
						if (this.FillInAllowed && this.FillInElement.checked === true) {
							values.push(this.FillInTextbox.getValue());
						}
						
						return values;
					};
					
					this.SetValue = function (value, checked) {
						// find the radio button we need to set in our hashtable
						var checkBox = this.Checkboxes.get(value);
						
						// if couldn't find the element in the hashtable
						// and fill-in is allowed, assume they meant the fill-in value
						if (Object.isUndefined(checkBox) && this.FillInAllowed) {
							checkBox = this.FillInElement;
							this.FillInTextbox.setValue(value);
						}
						
						if (!Object.isUndefined(checkBox)) {						
							if (!Object.isUndefined(checked)) {
								checkBox.checked = (true === checked);
							} else {
								checkBox.checked = true;
							}
						}
						updateReadOnlyLabel(this);
						return this;
					};
					
					// display as semicolon delimited list
					this.MakeReadOnly = function (options) {
						return makeReadOnly(this, arrayToSemicolonList(this.GetValue()));
					};
				} else {
					this.RadioButtons = getHashFromInputControls(this, RADIO_SELECTOR);
					
					// remove the fill-in radio button because we will check it separately
					if (this.FillInAllowed) {
						this.FillInElement = this.RadioButtons.get(FILL_IN_VALUE);
						this.RadioButtons.unset(FILL_IN_VALUE);
					}
					
					this.GetValue = function () {
						var value = null;
						// find the radio button we need to get in our hashtable
						this.RadioButtons.each(function (pair) {
							var radioButton = pair.value;
							if (true === radioButton.checked) {
								value = pair.key;
								throw $break;
							}
						});
						
						if (this.FillInAllowed && value === null && this.FillInElement.checked === true) {
							value = this.FillInTextbox.getValue();
						}
						
						return value;
					};
					
					this.SetValue = function (value) {
						// find the radio button we need to set in our hashtable
						var radioButton = this.RadioButtons.get(value);
						
						// if couldn't find the element in the hashtable and fill-in
						// is allowed, assume they want to set the fill-in value
						if (Object.isUndefined(radioButton) && this.FillInAllowed) {
							radioButton = this.FillInElement;
							this.FillInTextbox.setValue(value);
						}
						
						if (!Object.isUndefined(radioButton)) {
							radioButton.checked = true;
						}
						updateReadOnlyLabel(this);
						return this;
					};
				}
			}
		}
	});
	
	/*
	 *	SPDateTimeFieldValue class
	 *	Used to set/get values for SPDateTimeField fields
	 */
	SPDateTimeFieldValue = Class.create({
		initialize: function (year, month, day, strHour, strMinute) {
			this.Year = year;
			this.Month = month;
			this.Day = day;
			this.Hour = strHour;
			this.Minute = strMinute;
			this.IsTimeIncluded = !Object.isUndefined(this.Hour) && !Object.isUndefined(this.Minute);
			
			if (this.IsTimeIncluded) {
				if (!this.IsValidHour(this.Hour)) {
					throw 'Hour parameter is not in the correct format. Needs to be formatted like "1 PM" or "12 AM".';
				}
				if (!this.IsValidMinute(this.Minute)) {
					throw 'Minute parameter is not in the correct format. Needs to be formatted like "00", "05" or "35".';
				}
			}
		},
		
		/*
		 *	SPDateTimeFieldValue Public Methods
		 */
		
		IsValidDate: function () {
			return !Object.isUndefined(this.Year) && !Object.isUndefined(this.Month) && !Object.isUndefined(this.Day);
		},
		
		IsValidHour: function (h) {
			return !Object.isUndefined(h) && (/^([1-9]|10|11|12) (AM|PM)$/).test(h);
		},
			
		IsValidMinute: function (m) {
			return !Object.isUndefined(m) && (/^([0-5](0|5))$/).test(m);
		},
		
		// returns the part of a date as a string and pads with a 0 if necessary
		PadWithZero: function (d) {
			if (Object.isUndefined(d) || null === d) {
				return '';
			}
			if (typeof d === 'string') {
				d = parseInt(d, 10);
				if (isNaN(d)) {
					return '';
				}
			}
			if (typeof d === 'number' && d < 10) {
				return '0' + d.toString();
			}
			return d.toString();
		},
		
		// transforms a date object into a string
		GetShortDateString: function () {
			if (!this.IsValidDate()) {
				return '';
			}
			var strDate = this.PadWithZero(this.Month) + "/" +
				this.PadWithZero(this.Day) + "/" +
				this.PadWithZero(this.Year);
			return strDate;
		},
		
		toString: function () {
			var str = this.GetShortDateString(), arrHour;
			if (this.IsValidHour(this.Hour) && this.IsValidMinute(this.Minute)) {
				arrHour = this.Hour.split(' ');
				str += ' ' + arrHour[0] + ':' + this.Minute + arrHour[1];
			}
			return str;
		}
	});
	
	/*
	 *	SPDateTimeField class
	 *	Date and DateTime fields
	 */
	SPDateTimeField = Class.create(SPField, {
		initialize: function ($super, spParams) {
			$super(spParams);
			
			this.DateTextbox = getInputControl(this);
			
			this.HourDropdown = null;
			this.MinuteDropdown = null;
			var timeControls = this.Controls.select('select');
			if (null !== timeControls && 2 === timeControls.length) {
				this.HourDropdown = $(timeControls[0]);
				this.MinuteDropdown = $(timeControls[1]);
			}
		},
		
		/*
		 *	SPDateTimeField Public Methods
		 *	Overrides SPField class methods.
		 */
		GetValue: function () {
			var strHour, strMinute, arrShortDate,
				strShortDate = this.DateTextbox.getValue();

			if (null !== this.HourDropdown && null !== this.MinuteDropdown) {
				strHour = this.HourDropdown.getValue();
				strMinute = this.MinuteDropdown.getValue();
			}
			
			arrShortDate = strShortDate.split('/');
			
			if (arrShortDate.length === 3) {
				return new SPDateTimeFieldValue(arrShortDate[2], arrShortDate[0], arrShortDate[1], strHour, strMinute);
			}
			
			// empty or invalid date
			return '';
		},
		
		SetValue: function (year, month, day, strHour, strMinute) {
			if (Object.isString(year) && Object.isUndefined(month)) {
				// one string param passed to SetValue
				// assume they know what they are doing
				this.DateTextbox.setValue(year);
			} else {
				var value = new SPDateTimeFieldValue(year, month, day, strHour, strMinute);
				this.DateTextbox.setValue(value.GetShortDateString());
				if (null !== this.HourDropdown && null !== this.MinuteDropdown) {
					this.HourDropdown.setValue(value.Hour);
					this.MinuteDropdown.setValue(value.Minute);
				}
			}
			updateReadOnlyLabel(this);
			return this;
		}
		
	});
	
	/*
	 *	SPUtility Public Methods
	 */
	 
	return {
		// Creates a soap envelope for use with an AJAX call to SharePoint web services
		CreateSoapEnvelope: function (action, namespace, parameters) {
			var soap = '<?xml version="1.0" encoding="utf-8"?>';
			soap += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">';
			soap += '  <soap:Body>';
			
			// creat body of the soap envelope
			if (typeof parameters === 'object' && $H(parameters).keys().length > 0) {
				soap += '    <' + action + ' xmlns="' + namespace + '">';
				$H(parameters).each(function (pair) {
					soap += "<" + pair.key + ">" + pair.value + "</" + pair.key + ">";
				});
				soap += '    </' + action + '>';
			} else {
				soap += '    <' + action + ' xmlns="' + namespace + '" />';
			}
			
			soap += '  </soap:Body>';
			soap += '</soap:Envelope>';
			return soap;
		},
		
		Debug: function (isDebug) {
			if ('boolean' === typeof isDebug) {
				_debugMode = isDebug;
			}
			return _debugMode;
		},
	
		// Gets all of the SPFields on the page
		GetSPFields: function () {
			lazyLoadSPFields();
			return _fieldsHashtable;
		},
		
		// Searches the page for a specific field by name
		GetSPField: function (strFieldName) {
			lazyLoadSPFields();
			
			var fieldParams = _fieldsHashtable.get(strFieldName);
			
			if (Object.isUndefined(fieldParams)) { 
				log('GetSPField: Unable to find a SPField named ' + strFieldName);
				return null;
			}
			
			if (fieldParams.spField === null) {
				// field hasn't been initialized yet
				fieldParams.spField = createSPField(fieldParams);
			}
			
			return fieldParams.spField;
		},
		
		HideSPField: function (strFieldName) {
			toggleSPField(strFieldName, false);
		},
		
		ShowSPField: function (strFieldName) {
			toggleSPField(strFieldName, true);
		}
	};
}());
